package ch.ethz.fcl.metrobuzz.render.techniques.circos.version1;

import java.util.List;

import javax.vecmath.Vector2d;

import org.lwjgl.opengl.GL11;

import ch.ethz.fcl.metrobuzz.algorithm.sort.SortData;
import ch.ethz.fcl.metrobuzz.algorithm.sort.heapsort.HeapSort;
import ch.ethz.fcl.metrobuzz.render.techniques.bezier.Bezier;
import ch.ethz.fcl.metrobuzz.render.techniques.colors.MBColorTable;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Point2D;
import ch.ethz.fcl.metrobuzz.tools.MBGL;
import ch.ethz.fcl.metrobuzz.tools.MBTools;
import ch.ethz.fcl.metrofont.MetroFont;
import ch.ethz.fcl.metrofont.Pen;

/**
 * 
 * @author Zeng Wei
 * @email zeng@arch.ethz.ch
 * 
 *        Initial idea got from http://circos.ca/
 */

public class CircosDiagram1 {
	private Point2D center;
	private CircosMatrix1 data;

	Point2D[][] outgoingStartPoints;
	Point2D[][] outgoingEndPoints;
	Point2D[][] incomingStartPoints;
	Point2D[][] incomingEndPoints;
	int[] colorIndex;

	private float innerRadius;
	private float totalSum;

	private float radius;

	private final float outCircleOutRatio = 11.0f / 10;
	private final float outCircleInRatio = 30.0f / 31;

	private final float inCircleRatio = 11.0f / 10;
	private float renderAngle = (float) Math.PI;

	private final float minRenderAngle = (float) (Math.PI / (360 * 4));

	public CircosDiagram1(Point2D center, float radius, CircosMatrix1 data) {
		data.postProcess();

		this.radius = radius;
		this.innerRadius = radius * 0.45f;
		this.center = center;
		this.data = data;
		init();
	}

	public void render(MetroFont fm, boolean displayNum) {
		if (totalSum == 0)
			return;
		renderDistribution();
		fillHole();
		renderOuterCircle();
		renderMark(displayNum);
		renderNum(fm, displayNum);
	}

	private void init() {
		totalSum = 0;
		float[][] distri = data.getData();
		float[] outgoing = data.getOutgoing();
		float[] incoming = data.getIncoming();
		float[] dataSum = data.getSum();
		colorIndex = data.getColorIndex();

		for (int i = 0; i < data.size(); i++) {
			totalSum += outgoing[i];
			totalSum += incoming[i];
		}

		if (totalSum <= 0)
			return;

		MBTools.debug("total sum " + totalSum, false);
		initDirections(); // set new directions

		Vector2d[] dirs = data.getDirections();
		int[][] outRenderOrder = data.getOutRenderOrder();
		int[][] inRenderOrder = data.getInRenderOrder();
		int[] renderOrder = data.getRenderOrder();

		outgoingStartPoints = new Point2D[data.size()][data.size()];
		outgoingEndPoints = new Point2D[data.size()][data.size()];
		incomingStartPoints = new Point2D[data.size()][data.size()];
		incomingEndPoints = new Point2D[data.size()][data.size()];

		for (int i = 0; i < renderOrder.length; i++) {
			int renderIndex = renderOrder[i];

			float eclipseAngle = renderAngle
					* (outgoing[renderIndex] + incoming[renderIndex])
					/ (totalSum - data.getOutgoing()[0] - data.getIncoming()[0]);

			MBTools.debug("index " + renderIndex + " sum "
					+ (outgoing[renderIndex] + incoming[renderIndex]), false);

			Vector2d startDir = MBTools.rotate(dirs[renderIndex],
					-eclipseAngle / 2);

			Vector2d outgoingDir = startDir;
			for (int j = 0; outRenderOrder[renderIndex] != null
					&& j < outRenderOrder[renderIndex].length; j++) {
				int sortedIndex = outRenderOrder[renderIndex][j];
				Point2D outgoingStart = center.getPointAt(outgoingDir, radius);
				float outgoingAngle = distri[renderIndex][sortedIndex]
						/ dataSum[renderIndex] * eclipseAngle;

				outgoingStartPoints[renderIndex][sortedIndex] = outgoingStart;

				outgoingDir = MBTools.rotate(outgoingDir, outgoingAngle);

				outgoingEndPoints[renderIndex][sortedIndex] = center
						.getPointAt(outgoingDir, radius);
			}

			float outgoingAngle = outgoing[renderIndex] / dataSum[renderIndex]
					* eclipseAngle;
			Vector2d incomingDir = MBTools.rotate(startDir, outgoingAngle);
			for (int j = 0; inRenderOrder[renderIndex] != null
					&& j < inRenderOrder[renderIndex].length; j++) {
				int sortedIndex = inRenderOrder[renderIndex][j];
				Point2D incomingStart = center.getPointAt(incomingDir, radius
						* outCircleInRatio);
				float incomingAngle = distri[sortedIndex][renderIndex]
						/ dataSum[renderIndex] * eclipseAngle;

				incomingStartPoints[sortedIndex][renderIndex] = incomingStart;

				incomingDir = MBTools.rotate(incomingDir, incomingAngle);

				incomingEndPoints[sortedIndex][renderIndex] = center
						.getPointAt(incomingDir, radius * outCircleInRatio);
			}
		}
	}

	private void renderDistribution() {
		if (outgoingStartPoints == null || incomingEndPoints == null
				|| outgoingEndPoints == null || incomingStartPoints == null)
			return;

		HeapSort sort = new HeapSort();
		for (int i = 0; i < data.size(); i++)
			for (int j = 0; j < data.size(); j++)
				sort.addData(new SortData(data.getData()[i][j], i * data.size()
						+ j));
		List<SortData> sortDistri = sort.getSortedData();

		for (int k = 0; k < sortDistri.size(); k++) {
			int index = sortDistri.get(k).getIndex();
			int row = index / data.size();
			int col = index % data.size();

			float distribution = data.getData()[row][col];
			if (distribution / (totalSum - data.getSum()[row]) < minRenderAngle)
				continue;

			renderTableHalos(row, col);
			renderTable(row, col);
		}
	}

	private void renderTableHalos(int i, int j) {
		int curvePoints = 100;

		if (i != j)
			if (i != 0 && j != 0) {
				Point2D[] ctrlPoints1 = { outgoingStartPoints[i][j], center,
						incomingEndPoints[i][j] };
				Point2D[] ctrlPoints2 = { outgoingEndPoints[i][j], center,
						incomingStartPoints[i][j] };

				Point2D[] curve1 = Bezier.getBezierSpline(curvePoints,
						ctrlPoints1);
				Point2D[] curve2 = Bezier.getBezierSpline(curvePoints,
						ctrlPoints2);

				MBColorTable.assignColor(MBColorTable.WHITE, 1.0f);
				GL11.glLineWidth(5.0f);
				GL11.glBegin(GL11.GL_LINE_STRIP);
				for (int k = 0; k < curvePoints - 1; k++) {
					// only halo when line is inside the circle
					if (curve1[k].distance(center) < innerRadius
							* inCircleRatio
							&& curve1[k + 1].distance(center) < innerRadius
									* inCircleRatio)
						MBGL.render(curve1[k]);
				}
				GL11.glEnd();

				GL11.glBegin(GL11.GL_LINE_STRIP);
				for (int k = 0; k < curvePoints - 1; k++) {
					if (curve1[k].distance(center) < innerRadius
							* inCircleRatio
							&& curve1[k + 1].distance(center) < innerRadius
									* inCircleRatio)
						MBGL.render(curve2[k]);
				}
				GL11.glEnd();
			}
	}

	// if two eclipse overlap each other, rotate them until no eclipse overlap
	private void initDirections() {
		boolean check = true;
		while (check) {
			check = false;
			data.postProcess();

			Vector2d[] dirs = data.getDirections();
			int[] renderOrder = data.getRenderOrder();
			float[] eclipseAngles = new float[dirs.length];

			for (int i = 0; i < renderOrder.length; i++) {
				int thisIndex = renderOrder[i];
				int preIndex = renderOrder[(i + renderOrder.length - 1)
						% renderOrder.length];

				float dirAngle = (float) MBTools.angle(dirs[thisIndex],
						dirs[preIndex]);
				if (dirAngle <= 0) { // if clockwise
					Vector2d preEndDir = MBTools.rotate(dirs[preIndex],
							eclipseAngles[preIndex] / 2);

					Vector2d curStartDir = MBTools.rotate(dirs[thisIndex],
							-eclipseAngles[thisIndex] / 2);
					float angle = (float) MBTools.angle(curStartDir, preEndDir);

					MBTools.debug("Previous index " + preIndex + " end dir "
							+ preEndDir.x + " " + preEndDir.y, false);
					MBTools.debug("This index " + thisIndex + " start dir "
							+ curStartDir.x + " " + curStartDir.y, false);
					MBTools.debug("Angle " + angle, false);

					// if two eclipse overlap, shift cur direction
					// anti-clockwise
					if (angle > -Math.PI / 30) {
						float rotate = (float) ((angle + Math.PI / 30) / 2);

						data.setDirection(thisIndex,
								MBTools.rotate(dirs[thisIndex], rotate));
						data.setDirection(preIndex,
								MBTools.rotate(dirs[preIndex], -rotate));

						MBTools.debug(thisIndex + " anti rotate " + rotate,
								false);
						MBTools.debug(preIndex + " rotate " + rotate, false);

						check = true;
					}
				} else { // anti clockwise
					Vector2d preStartDir = MBTools.rotate(dirs[preIndex],
							-eclipseAngles[preIndex] / 2);

					Vector2d curEndDir = MBTools.rotate(dirs[thisIndex],
							eclipseAngles[thisIndex] / 2);
					float angle = (float) MBTools.angle(curEndDir, preStartDir);

					MBTools.debug("Previous index " + preIndex + " start dir "
							+ preStartDir.x + " " + preStartDir.y, false);
					MBTools.debug("This index " + thisIndex + " start dir "
							+ curEndDir.x + " " + curEndDir.y, false);
					MBTools.debug("Angle " + angle, false);

					// if two eclipse overlap, shift cur direction
					// clockwise
					if (angle < Math.PI / 30) {
						float rotate = (float) ((-angle + Math.PI / 30) / 2);

						data.setDirection(thisIndex,
								MBTools.rotate(dirs[thisIndex], -rotate));
						data.setDirection(preIndex,
								MBTools.rotate(dirs[preIndex], rotate));

						MBTools.debug(thisIndex + " rotate " + rotate, false);
						MBTools.debug(preIndex + " anti rotate " + rotate,
								false);

						check = true;
					}
				}

			}
		}
	}

	private void renderTable(int i, int j) {
		int curvePoints = 100;

		if (i != j) {
			Point2D[] ctrlPoints1 = { outgoingStartPoints[i][j], center,
					incomingEndPoints[i][j] };
			Point2D[] ctrlPoints2 = { outgoingEndPoints[i][j], center,
					incomingStartPoints[i][j] };

			Point2D[] curve1 = Bezier.getBezierSpline(curvePoints, ctrlPoints1);
			Point2D[] curve2 = Bezier.getBezierSpline(curvePoints, ctrlPoints2);

			GL11.glBegin(GL11.GL_TRIANGLE_STRIP);

			for (int k = 0; k <= curvePoints; k++) {
				MBColorTable.assignGradualColor(colorIndex[i], colorIndex[j],
						1.0f * k / curvePoints, 1.0f, 3);
				GL11.glVertex3f(curve1[k].getX(), curve1[k].getY(), 0);
				GL11.glVertex3f(curve2[k].getX(), curve2[k].getY(), 0);
			}

			GL11.glEnd();

			GL11.glLineWidth(1.0f);
			GL11.glColor4f(0.0f, 0.0f, 0.0f, 0.5f);
			GL11.glBegin(GL11.GL_LINE_STRIP);
			for (int k = 0; k <= curvePoints; k++) {
				GL11.glVertex3f(curve1[k].getX(), curve1[k].getY(), 0);
			}
			GL11.glEnd();
			GL11.glBegin(GL11.GL_LINE_STRIP);
			for (int k = 0; k <= curvePoints; k++) {
				GL11.glVertex3f(curve2[k].getX(), curve2[k].getY(), 0);
			}
			GL11.glEnd();
		}
	}

	private void fillHole() {
		for (int i = 0; i < data.size(); i++) {
			int[] outOrder = data.getOutRenderOrder()[i];
			for (int j = 0; j < outOrder.length; j++) {
				fillHole(i, outgoingStartPoints[i][outOrder[j]],
						outgoingEndPoints[i][outOrder[j]], radius, false);
			}

			int[] inOrder = data.getInRenderOrder()[i];
			for (int j = 0; j < inOrder.length; j++) {
				float distribution = data.getData()[inOrder[j]][i];
				if (distribution / (totalSum - data.getSum()[inOrder[j]]) < minRenderAngle)
					continue;
				fillHole(i, incomingStartPoints[inOrder[j]][i],
						incomingEndPoints[inOrder[j]][i], radius
								* outCircleInRatio, true);
			}
		}
	}

	private void fillHole(int index, Point2D start, Point2D end, float radius,
			boolean renderOutline) {
		Vector2d startDir = new Vector2d(start.getX() - center.getX(),
				start.getY() - center.getY());
		Vector2d endDir = new Vector2d(end.getX() - center.getX(), end.getY()
				- center.getY());

		float angle = (float) MBTools.angle(startDir, endDir);

		fillHole(index, start, radius, angle, renderOutline);
	}

	private void fillHole(int index, Point2D start, float radius, float angle,
			boolean renderOutline) {
		Vector2d dir = new Vector2d(start.getX() - center.getX(), start.getY()
				- center.getY());
		Point2D end = center.getPointAt(MBTools.rotate(dir, angle), radius);
		Point2D[] ctrlPoints = { start, end };

		Point2D[] eclipsePoints = getEclipsePoints(center, radius, dir, angle);
		Point2D[] linePoints = Bezier.getBezierSpline(eclipsePoints.length,
				ctrlPoints);

		MBColorTable.assignColor(colorIndex[index], 1.0f);
		GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
		for (int i = 0; i < eclipsePoints.length; i++) {
			GL11.glVertex3f(eclipsePoints[i].getX(), eclipsePoints[i].getY(), 0);
			GL11.glVertex3f(linePoints[i].getX(), linePoints[i].getY(), 0);
		}
		GL11.glEnd();
		
		if (renderOutline) {
			GL11.glLineWidth(1.0f);
			GL11.glColor4f(0.0f, 0.0f, 0.0f, 0.5f);
			GL11.glBegin(GL11.GL_LINE_STRIP);
			for (int i = 0; i < eclipsePoints.length; i++) {
				GL11.glVertex3f(eclipsePoints[i].getX(),
						eclipsePoints[i].getY(), 0);
			}
			GL11.glEnd();
		}
	}

	private void rendEclipse(int index, Point2D start, Point2D center,
			float radius, float angle) {
		if (angle < minRenderAngle)
			return;

		Vector2d dir = new Vector2d(start.getX() - center.getX(), start.getY()
				- center.getY());
		Point2D[] innerPoints = getEclipsePoints(center, radius, dir, angle);
		Point2D[] outerPoints = getEclipsePoints(center, radius
				* outCircleOutRatio, dir, angle);

		GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
		for (int i = 0; i < innerPoints.length; i++) {
			GL11.glVertex3f(innerPoints[i].getX(), innerPoints[i].getY(), 0);

			GL11.glVertex3f(outerPoints[i].getX(), outerPoints[i].getY(), 0);
		}
		GL11.glEnd();

		float outAngle = angle * data.getOutgoing()[index]
				/ data.getSum()[index];
		Point2D[] innerBoundPoints = getEclipsePoints(center, radius,
				MBTools.rotate(dir, outAngle), angle - outAngle);

		GL11.glLineWidth(1.0f);
		GL11.glColor4f(0.0f, 0.0f, 0.0f, 0.5f);
		GL11.glBegin(GL11.GL_LINE_STRIP);
		for (int i = 0; i < innerBoundPoints.length; i++)
			GL11.glVertex3f(innerBoundPoints[i].getX(),
					innerBoundPoints[i].getY(), 0);
		for (int i = innerPoints.length - 1; i >= 0; i--)
			GL11.glVertex3f(outerPoints[i].getX(), outerPoints[i].getY(), 0);
		GL11.glVertex3f(innerPoints[0].getX(), innerPoints[0].getY(), 0);
		GL11.glEnd();
	}

	private Point2D[] getEclipsePoints(Point2D center, float radius,
			Vector2d dir, float angle) {
		float delta = 0;
		if (angle > 0)
			delta = 2 * (float) Math.PI / 360; // 1 degree
		if (angle < 0)
			delta = -2 * (float) Math.PI / 360; // -1 degree
		int size = (int) (angle / delta) + 1;

		Point2D[] points = new Point2D[size + 1];
		for (int i = 0; i < size; i++) {
			points[i] = center.getPointAt(MBTools.rotate(dir, delta * i),
					radius);
		}
		points[size] = center.getPointAt(MBTools.rotate(dir, angle), radius);

		return points;
	}

	private void renderOuterCircle() {
		for (int i = 0; i < data.size(); i++) {
			MBColorTable.assignColor(colorIndex[i], 1.0f);
			int[] outOrder = data.getOutRenderOrder()[i];

			float eclipseAngle = renderAngle
					* (data.getOutgoing()[i] + data.getIncoming()[i])
					/ (totalSum - data.getOutgoing()[0] - data.getIncoming()[0]);

			rendEclipse(i, outgoingStartPoints[i][outOrder[0]], center, radius,
					eclipseAngle);
		}
	}

	private void renderNum(MetroFont fm, boolean display) {
		if (fm == null || !display)
			return;

		if (totalSum <= 0)
			return;

		float fontSize = (outCircleOutRatio - 1) * radius * 0.75f;

		for (int i = 1; i < data.size(); i++) {
			float perc = data.getSum()[i]
					/ (totalSum - data.getOutgoing()[0] - data.getIncoming()[0]);
			float angle = perc * renderAngle;

			int[] outOrder = data.getOutRenderOrder()[i];

			Vector2d startDir = new Vector2d(
					outgoingStartPoints[i][outOrder[0]].getX() - center.getX(),
					outgoingStartPoints[i][outOrder[0]].getY() - center.getY());

			int range = (int) (perc * 100);
			for (int j = 0; j <= range; j++) {
				Vector2d dir = MBTools.rotate(startDir, angle * j / range);
				if (j % 5 == 0 && range - j > 2) {
					// Vector2d fontDir = MBTools.rotate(dir, -(float) fontSize
					// / (2 * 360 * averageRadius));
					float fontAngle = (float) MBTools.angle(new Vector2d(1, 0),
							dir);

					Point2D point = center.getPointAt(dir, radius
							* outCircleOutRatio
							* (1 + (outCircleOutRatio - 1) / 2));
					Pen pen = new Pen(point.getX(), point.getY(), 0, fontAngle,
							0, fontSize);
					fm.display(j + "%", pen);
				}
			}

			if (angle > minRenderAngle) {
				Vector2d dir = MBTools.rotate(startDir, angle);
				Point2D point = center
						.getPointAt(dir, radius * outCircleOutRatio
								* (1 + (outCircleOutRatio - 1) / 2));
				float fontAngle = (float) MBTools
						.angle(new Vector2d(1, 0), dir);
				Pen pen = new Pen(point.getX(), point.getY(), 0, fontAngle, 0,
						fontSize);
				fm.display((int) data.getSum()[i] + "", pen);
			}
		}
	}

	private void renderMark(boolean render) {
		if (!render)
			return;

		for (int i = 1; i < data.size() && outgoingStartPoints != null; i++) {
			float perc = data.getSum()[i] / (totalSum - data.getSum()[0]);
			float angle = perc * renderAngle;

			int[] outOrder = data.getOutRenderOrder()[i];
			renderMark(outgoingStartPoints[i][outOrder[0]], angle,
					(int) (perc * 100), radius * outCircleOutRatio);
		}
	}

	private void renderMark(Point2D start, float angle, int range, float radius) {
		Vector2d startDir = new Vector2d(start.getX() - center.getX(),
				start.getY() - center.getY());

		GL11.glColor4f(0.0f, 0.0f, 0.0f, 0.7f);
		GL11.glLineWidth(1.0f);
		GL11.glBegin(GL11.GL_LINES);
		for (int i = 0; i < range; i++) {
			Vector2d dir = MBTools.rotate(startDir, angle * i / range);
			if (i % 5 == 0)
				renderMark(dir, radius, radius * (outCircleOutRatio - 1) / 2);
			else
				renderMark(dir, radius, radius * (outCircleOutRatio - 1) / 4);
		}
		GL11.glEnd();
	}

	private void renderMark(Vector2d dir, float radius, float length) {
		Point2D p1 = center.getPointAt(dir, radius);
		Point2D p2 = center.getPointAt(dir, radius + length);

		MBGL.render(p1);
		MBGL.render(p2);
	}
}